# -*- coding: utf-8 -*-
import os
import json
import uuid
import collections
from datetime import datetime
from functools import wraps
import socket
import traceback
import logging
import threading
from multiprocessing.dummy import (
    Pool as ThreadPool,
    Lock
)
import sys
import signal
import multiprocessing as mp

from werkzeug.routing import BaseConverter
from werkzeug.local import LocalProxy
from flask import (
    Flask,
    g,
    session,
    render_template,
    render_template_string,
    redirect,
    url_for,
    current_app,
    abort,
    request,
    make_response,
    jsonify
)
import psutil
import pytz

from dbhelper import DBHelper
from visit_record import VisitRecord
from print_log_client import print_log


_is_debug = False
if len(sys.argv) > 1:
    if '--debug' in sys.argv[1:]:
        _is_debug = True

SEP = os.linesep
ROOT = os.path.dirname(os.path.abspath(__file__))
template_folder = os.path.join(ROOT, 'frontend/templates')
static_folder = os.path.join(ROOT, 'frontend/static')

app = Flask(
    __name__,
    template_folder=template_folder,
    static_folder=static_folder
)
app.config['SECRET_KEY'] = 'gsw945 website secret ^_^'
app.debug = _is_debug

db_file = os.path.join(ROOT, 'data.sqlite')
uri = 'sqlite:///{0}'.format(db_file)
app.db = DBHelper(uri)

vr_file = os.path.join(ROOT, 'visit-record.sqlite')
vr_uri = 'sqlite:///{0}'.format(vr_file)

cpu_count = psutil.cpu_count()
tz = pytz.timezone('Asia/Shanghai')

def print_log_server(port=10909):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    addr = ('0.0.0.0', port)
    log_file = os.path.join(ROOT, 'print.log')
    start_msg = 'udp log server listen at [{0}:{1}]'.format(addr[0], addr[1])
    with open(log_file,'a') as f:
        f.write(start_msg)
        f.write(SEP)
    print(start_msg)
    try:
        sock.bind(addr)
        while True:
            data, address = sock.recvfrom(4096)
            if bool(data):
                # sent = sock.sendto(data, address)
                msg = data.decode('utf-8', 'ignore')
                # print('get data [{0}]'.format(msg))
                with open(log_file,'a') as f:
                    f.write(msg)
                    f.write(SEP)
    except (KeyboardInterrupt, SystemExit, Exception):
        # print(traceback.format_exc())
        print('=' * 60, sep='')
        traceback.print_exc()
        print('=' * 60)
    finally:
        sock.close()

def update_logger(logger, log_file=None):
    # refer: https://docs.python.org/3.5/library/logging.html#logrecord-attributes
    formatter = logging.Formatter(
        fmt='\n'.join([
            '[%(name)s] %(asctime)s.%(msecs)d',
            '\t%(pathname)s [line: %(lineno)d]',
            '\t%(processName)s[%(process)d] => %(threadName)s[%(thread)d] => %(module)s.%(filename)s:%(funcName)s()',
            '\t%(levelname)s: %(message)s\n'
        ]),
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    if log_file is None:
        log_file = 'logger.log'
    # stream_handler = logging.StreamHandler()
    # stream_handler.setFormatter(formatter)
    # logger.addHandler(stream_handler)

    file_handler = logging.FileHandler(log_file, mode='a', encoding='utf-8')
    file_handler.setFormatter(formatter)
    logger.addHandler(file_handler)

    logger.setLevel(logging.DEBUG)

    return logger

app_obj = LocalProxy(lambda: current_app._get_current_object())
app_db = LocalProxy(lambda: current_app.db.db)
app_tpp = LocalProxy(lambda: current_app.tpp)

_logger = logging.getLogger('flask.app')
new_logger = update_logger(_logger)
tp_lock = None
log_port = None
ts_fmt = '%Y-%m-%d %H:%M:%S.%f'

class RegexConverter(BaseConverter):
    def __init__(self, _map, *args):
        self.map = _map
        self.regex = args[0]

app.url_map.converters['regex'] = RegexConverter

def get_http_exception_handler(app):
    """
    # refer: https://stackoverflow.com/questions/29332056/global-error-handler-for-any-exception#44083675
    Overrides the default http exception handler to return JSON.
    """
    handle_http_exception = app.handle_http_exception
    @wraps(handle_http_exception)
    def ret_val(exception):
        exc = handle_http_exception(exception)
        is_ajax = request.headers.get('X-Requested-With', None) == 'XMLHttpRequest'
        is_api = 'X-Api' in request.headers
        resp = ''
        err = {
            'code': exc.code,
            'name': exc.name,
            'description': exc.description
        }
        if is_ajax or is_api:
            resp = jsonify(err)
        else:
            tmpl = ''.join([
                '<!DOCTYPE html>',
                '<html>',
                '<head>',
                    '<title>{{ code }} {{ name }}</title>',
                '</head>',
                '<body>',
                    '<h2>{{ code }} {{ name }}</h2>',
                    '<p>{{ description }}</p>',
                    '<p>&#8674;&#8608; <a href="/">Home Page</a></p>'
                '</body>',
                '</html>'
            ])
            resp = render_template_string(tmpl, **err)
        return resp, exc.code
    return ret_val

# Override the HTTP exception handler.
app.handle_http_exception = get_http_exception_handler(app)

def log_visit_record(vr_uri, vr_data, lock=None, log_port=None):
    thread = threading.current_thread()
    print(thread)
    lock.acquire()
    vr_db = VisitRecord(vr_uri)
    records = vr_db.db['records']
    affect = records.insert(vr_data)
    vr_db.close()
    if affect > 0:
        print_log('write visit record log completed', log_port)
    else:
        print_log('write visit record log failed', log_port)
    lock.release()
    return None

@app.before_request
def before_request():
    global app_obj
    if app_obj.debug:
        app_obj.jinja_env.cache = {}
    print('before request')

@app.after_request
def after_request(response):
    g.is_static = request.endpoint == 'static'
    if not g.is_static:
        coks = request.cookies
        _uid = coks.get('uid', None)
        regular = True
        if _uid is None:
            regular = False
            _uid = uuid.uuid5(uuid.NAMESPACE_DNS, '').hex
            _max_age = 3600 * 24 * 30 * 12 # 1 year
            response.set_cookie('uid', _uid, max_age=_max_age)
        g.uid = _uid
        g.regular = regular
        g.ts_now = datetime.now(tz).strftime(ts_fmt)
    print('after request')
    return response

@app.teardown_request
def teardown_request(exception):
    if not g.is_static:
        global app_tpp

        global tp_lock
        global vr_uri
        global log_port
        global tz
        global ts_fmt

        hdrs = request.headers
        full_path = request.full_path
        is_xhr = request.is_xhr
        method = request.method
        scheme = request.scheme
        referer = hdrs.get('Referer', request.referrer)
        x_real_ip = hdrs.get('X-Real-Ip', request.remote_addr)
        x_forwarded_for = hdrs.get('X-Forwarded-For', None)
        accept_language = hdrs.get('Accept-Language', request.accept_languages)
        host = hdrs.get('Host', request.host)
        user_agent = hdrs.get('User-Agent', request.user_agent)
        uid = g.uid
        regular = g.regular
        dt_str = g.ts_now

        vr_data = {
            'uid': uid,
            'regular': regular,
            'method': method,
            'is_xhr': is_xhr,
            'scheme': scheme,
            'host': host,
            'x_real_ip': x_real_ip,
            'full_path': full_path,
            'referer': referer,
            'x_forwarded_for': x_forwarded_for,
            'accept_language': accept_language,
            'user_agent': user_agent,
            'dt_str': dt_str
        }
        apply_data = {
            'lock': tp_lock,
            'log_port': log_port
        }
        # r = app_tpp.apply(func=log_visit_record, args=(vr_uri, vr_data), kwds=apply_data)
        r = app_tpp.apply_async(func=log_visit_record, args=(vr_uri, vr_data), kwds=apply_data)
    
    print('teardown request')

def row2dict(row):
    row_json = {}
    for col_name in row:
        col_value = row[col_name]
        if isinstance(col_value, datetime):
            row_json[col_name] = col_value.strftime('%Y-%m-%d %H:%M:%S.%f')
        else:
            row_json[col_name] = col_value
    return row_json

def login_required(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        not_in_g = not hasattr(g, 'user') or g.user is None
        not_in_s = not 'user' in session or session['user'] is None
        if not_in_g and not_in_s:
            if 'confirm_route' in current_app.config:
                _route = current_app.config['confirm_route']
            else:
                _route = current_app.config['login_route']
            return redirect(url_for(_route, next=request.url))
        return func(*args, **kwargs)
    return decorated_function

def admin_required(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        in_g = hasattr(g, 'user') and not getattr(g, 'user') is None
        in_s = 'user' in session and not session['user'] is None
        if in_g or in_s:
            g_admin = in_g and getattr(g, 'user').is_admin
            s_admin = in_s and 'is_admin' in session['user'] and bool(session['user']['is_admin'])
            if g_admin or s_admin:
                return func(*args, **kwargs)
            else:
                return abort(403)
        else:
            if 'confirm_route' in current_app.config:
                _route = current_app.config['confirm_route']
            else:
                _route = current_app.config['login_route']
            return redirect(url_for(_route, next=request.url))
    return decorated_function


@app.route(r'/<regex("(favicon\.ico)|(robots\.txt)"):_file>')
def favicon(_file):
    _file = 'asset/' + _file
    asset_file = os.path.join(app.static_folder, _file)
    if os.path.exists(asset_file):
        return app.send_static_file(_file)
    else:
        return make_response('')

@app.route(r'/about<regex("[\/]?"):_suffix>')
def view_about(_suffix=None):
    return render_template('pages/about.html')

@app.route(r'/copyright<regex("[\/]?"):_suffix>')
def view_copyright(_suffix=None):
    return render_template('pages/copyright.html')

@app.route(r'/message<regex("[\/]?"):_suffix>')
def view_message(_suffix=None):
    return render_template('pages/message.html')

@app.route(r'/edit<regex("[\/]?"):_suffix>')
def view_edit(_suffix=None):
    global categories
    global app_db
    blog_id = request.values.get('id', None)
    tpl_params = dict(categories=categories)
    if not blog_id is None:
        is_found = False
        if blog_id.isdigit():
            blogs = app_db['blogs']
            blog_item = blogs.find_one(id=int(blog_id))
            if isinstance(blog_item, collections.OrderedDict):
                blog_item = row2dict(blog_item)
                tpl_params['blog_item'] = blog_item
                is_found = True
        if not is_found:
            abort(404)
    return render_template('pages/edit.html', **tpl_params)

@app.route(r'/<regex("[\/]?"):_suffix>')
def view_index(_suffix=None):
    global app_db
    global categories
    blogs = app_db['blogs']
    count = blogs.count()
    articles = blogs.find(_offset=0, _limit=10, order_by='-create_time')
    data = []
    for row in articles:
        data.append(row2dict(row))
    if bool(data):
        print(data[0].keys())
    return render_template('pages/index.html', blog_list=data, categories=categories)

@app.route(r'/detail/<int:blog_id>')
def view_detail(blog_id=None):
    global app_db
    blogs = app_db['blogs']
    blog_item = blogs.find_one(id=int(blog_id))
    if isinstance(blog_item, collections.OrderedDict):
        blog_item = row2dict(blog_item)
        return render_template('pages/detail.html', blog_item=blog_item)
    else:
        return abort(404)

@app.route(r'/save<regex("[\/]?"):_suffix>', methods=['POST'])
def ajax_save(_suffix=None):
    global app_db
    global tz
    title = request.values.get('title', None)
    markdown = request.values.get('markdown', None)
    sketch = request.values.get('sketch', None)
    if not bool(sketch):
        sketch = markdown[:150]
    html = request.values.get('html', None)
    category = request.values.get('category', None)
    tags = request.values.get('tags', None)
    blogs = app_db['blogs']
    blog_id = request.values.get('id', None)
    affect = 0
    blog_data = dict(
        title=title,
        markdown=markdown,
        sketch=sketch,
        html=html,
        category=category,
        tags=tags,
        create_time=datetime.now(tz)
    )
    if not blog_id is None:
        is_found = False
        if blog_id.isdigit():
            blog_item = blogs.find_one(id=int(blog_id))
            if isinstance(blog_item, collections.OrderedDict):
                blog_item.update(blog_data)
                affect = blogs.update(blog_item, ['id'])
                is_found = True
        if not is_found:
            abort(404)
    else:
        affect = blogs.insert(blog_data)
    if affect > 0:
        ret = {
            'error': 0,
            'msg': 'sucess'
        }
    else:
        ret = {
            'error': 1,
            'msg': 'server error'
        }
    return jsonify(ret)

@app.route(r'/list<regex("[\/]?"):_suffix>', methods=['GET', 'POST'])
def ajax_list(_suffix=None):
    global app_db
    blogs = app_db['blogs']
    count = blogs.count()
    articles = blogs.find(_offset=0, _limit=10, order_by='-create_time')
    data = []
    for row in articles:
        data.append(row2dict(row))
    return jsonify({
        'error': 0,
        'msg': 'success',
        'data': {
            'count': count,
            'rows': data
        }
    })

@app.route(r'/tags<regex("[\/]?"):_suffix>', methods=['GET', 'POST'])
def ajax_tags(_suffix=None):
    global app_db
    tb_tags = app_db['tags']
    count = tb_tags.count()
    tag_list = tb_tags.find()
    data = []
    for row in tag_list:
        data.append(row2dict(row))
    return jsonify({
        'error': 0,
        'msg': 'success',
        'data': {
            'count': count,
            'rows': data
        }
    })

@app.route(r'/ajax/log<regex("[\/]?"):_suffix>', methods=['GET', 'POST'])
def ajax_log(_suffix=None):
    job_id = request.values.get('job_id', None)
    ret = {
        'error': 1,
        'msg': '缺少参数'
    }
    return json.dumps(ret, ensure_ascii=False)

categories = [
    {
        'name': 'learn-study',
        'text': '学习&研究'
    },
    {
        'name': '',
        'text': ''
    },
    {
        'name': 'share',
        'text': '分享'
    },
    {
        'name': 'memo',
        'text': '备忘'
    }
]

def test():
    app = current_app._get_current_object()
    with app.app_context():
        print('current_app context test')

if __name__ == '__main__':
    if sys.platform == 'win32' or os.name == 'nt':
        mp.freeze_support()
    log_port = 10909
    log_process = mp.Process(
        name='log_process',
        target=print_log_server,
        args=(),
        kwargs={
            'port': log_port
        }
    )
    log_process.daemon = True
    try:
        log_process.start()

        # 实例化线程池需要在 '__main__' 里面
        app.tpp = ThreadPool(processes=cpu_count)
        tp_lock = Lock()
        debug = app.debug
        cfg = {
            'host': '0.0.0.0',
            'port': 5000
        }
        if not debug:
            # doc: https://docs.pylonsproject.org/projects/waitress/en/latest/#usage
            from waitress import serve
            serve(app, **cfg)
        else:
            print('visit by [http://{0}:{1}]'.format(cfg['host'], cfg['port']))
            cfg.update({
                'debug': debug,
                'use_reloader': True,
                'threaded': True
            })
            cfg['extra_files'] = [app.static_folder, app.template_folder]
            app.jinja_env.auto_reload = True
            app.config['TEMPLATES_AUTO_RELOAD'] = True
            app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
            app.run(**cfg)
    except (KeyboardInterrupt, SystemExit):
        pass
    finally:
        # log_process.terminate()
        log_process.join()